﻿
Imports System.Globalization
Imports System.Reflection
Imports System.Data
Imports System.Drawing
Imports System.Data.Objects
Imports System.Data.Objects.DataClasses
Imports System.Collections
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Windows.Forms
Imports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Diagnostics
Imports System.Linq
Imports System.Text

''' <summary>
''' DataSource component that encapsulates an EntityFramework ObjectContext and exposes
''' all ObjectSets in the context as binding EntitySets.
''' </summary>
''' <remarks>
''' For binding purposes, the EntityDataSource component corresponds to a DataSet and 
''' the EntitySets correspond to the contained DataTables. For example, to bind a grid 
''' to an object set you should set the grid's DataSource property to the DataSource 
''' component and the DataMember property to one of the EntitySets.
''' </remarks>r
<ProvideProperty("AutoLookup", GetType(Control))>
<ToolboxItem(True)>
<ToolboxBitmap(GetType(EntityDataSource), "EntityDataSource")>
<DefaultProperty("ObjectContextType")>
Partial Public Class EntityDataSource
    Inherits Component
    Implements IListSource
    Implements IExtenderProvider

#Region "Fields"

    Private m_ObjectContext As ObjectContext
    Private m_EntitySetCollection As New EntitySetCollection()
    Private m_ObjectContextType As Type
    'To support design-time

#End Region

#Region "Constructors"

    ''' <summary>
    ''' Initializes a new instance of a <see cref="EntityDataSource"/>.
    ''' </summary>
    Public Sub New()
        InitializeComponent()
    End Sub

    ''' <summary>
    ''' Initializes a new instance of a <see cref="EntityDataSource"/>.
    ''' </summary>
    Public Sub New(ByVal container As IContainer)
        container.Add(Me)
        InitializeComponent()
    End Sub

#End Region

#Region "Object model"

    ''' <summary>
    ''' Gets or sets the type of ObjectContext to use as a data source.
    ''' </summary>
    ''' <remarks>
    ''' This property is normally set at design time. Once it is set, the 
    ''' component will automatically create an ObjectContext of the appropriate type.
    ''' </remarks>
    <TypeConverter(GetType(ObjectContextTypeTypeConverter))>
    Public Property ObjectContextType() As Type
        Get
            Return m_ObjectContextType
        End Get
        Set(ByVal value As Type)
            If Not value Is m_ObjectContextType Then
                'Notify
                OnObjectContextTypeChanging(EventArgs.Empty)

                'Clear existing object context
                m_ObjectContext = Nothing

                'Set new object context type 
                m_ObjectContextType = value

                'Generate object sets (will re-create object context if appropriate)
                GenerateEntitySets(m_ObjectContextType)

                'Notify
                OnObjectContextTypeChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Gets or sets the ObjectContext used as a data source.
    ''' </summary>
    <Browsable(False), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
    Public Property ObjectContext() As ObjectContext
        Get
            If m_ObjectContext Is Nothing AndAlso m_ObjectContextType IsNot Nothing AndAlso Not DesignMode Then
                Try
                    Dim ctx As ObjectContext = TryCast(Activator.CreateInstance(DirectCast(m_ObjectContextType, Type)), ObjectContext)
                    Me.ObjectContext = ctx
                Catch
                End Try
            End If
            Return m_ObjectContext
        End Get
        Set(ByVal value As ObjectContext)
            If Not Object.Equals(m_ObjectContext, value) Then
                'Notify
                OnObjectContextChanging(EventArgs.Empty)

                'Save the new context
                m_ObjectContext = value

                'Update the context type
                m_ObjectContextType = If(m_ObjectContext IsNot Nothing, m_ObjectContext.GetType(), Nothing)

                'Generate object sets
                GenerateEntitySets(m_ObjectContextType)

                'Notify
                OnObjectContextChanged(EventArgs.Empty)
            End If
        End Set
    End Property

    ''' <summary>
    ''' Gets the collection of EntitySets available in this EntityDataSource.
    ''' </summary>
    <Browsable(False), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
    Public ReadOnly Property EntitySets() As EntitySetCollection
        Get
            Return m_EntitySetCollection
        End Get
    End Property

    ''' <summary>
    ''' Saves all changes made to all entity sets back to the database.
    ''' </summary>
    Public Function SaveChanges() As Integer
        'Notify
        Dim e = New CancelEventArgs()
        OnSavingChanges(e)

        'Save the changes
        Dim count As Integer = 0
        If Not e.Cancel Then
            Try
                count = m_ObjectContext.SaveChanges()
                Debug.WriteLine(String.Format("Done. {0} changes saved.", count))

                'Notify
                OnSavedChanges(e)
            Catch ex As Exception
                OnDataError(New DataErrorEventArgs(ex))
            End Try
        End If

        Return count
    End Function

    ''' <summary>
    ''' Cancels all changes made to all entity sets.
    ''' </summary>
    Public Sub CancelChanges()
        'Notify
        Dim e = New CancelEventArgs()
        OnCancelingChanges(e)

        If m_ObjectContext IsNot Nothing AndAlso Not e.Cancel Then
            Try
                For Each es As EntitySet In EntitySets
                    es.CancelChanges()
                Next
                m_ObjectContext.AcceptAllChanges()
            Catch ex As Exception
                OnDataError(New DataErrorEventArgs(ex))
            End Try

            'Notify
            OnCanceledChanges(EventArgs.Empty)
        End If
        Debug.WriteLine("Done. All changes cancelled.")
    End Sub

    ''' <summary>
    ''' Refreshes all views by loading their data from the database.
    ''' </summary>
    Public Sub Refresh()
        'Notify
        Dim e = New CancelEventArgs()
        OnRefreshing(e)

        If Not e.Cancel Then
            Try
                For Each es As EntitySet In EntitySets
                    es.RefreshView()
                Next
            Catch ex As Exception
                OnDataError(New DataErrorEventArgs(ex))
            End Try

            'Notify
            OnRefreshed(EventArgs.Empty)
            Debug.WriteLine("Done. All views refreshed.")
        End If
    End Sub

    ''' <summary>
    ''' Gets a lookup dictionary for a given element type.
    ''' </summary>
    ''' <param name="elementType">Type of element for which to return a lookup dictionary.</param>
    ''' <returns>A lookup dictionary for a given element type.</returns>
    ''' <remarks>
    ''' <para>The lookup dictionary has keys that correspond to the items on a list and
    ''' values that contain a string representation of the items.</para>
    ''' <para>When lists of entities are sorted on a column that contains entity references, 
    ''' the lookup dictionary is used to provide the sorting order. For example, if you sort a 
    ''' list of products by category, the data map associated with the Categories list determines 
    ''' the order in which the categories are compared while sorting.</para>
    ''' <para>Lookup dictionaries are especially useful for setting the DataMap property on the 
    ''' columns of a C1FlexGrid control. For example, if a C1FlexGrid is bound to a list of products,
    ''' and the Product entity contains references to other entities (e.g. Category or Supplier),
    ''' then the grid uses the lookup dictionary to provide a useful string representation of the 
    ''' related entities (e.g. category name). The grid also uses the data map to provide combo boxes 
    ''' for editing the cell values.</para>
    ''' <para>If you add a C1FlexGrid control to a form that contains a 
    ''' <see cref="EntityDataSource"/>, the data source will provide an extender property 
    ''' called <b>AutoLookup</b> that may be set on the grids to create and assign lookup 
    ''' dictionaries to their columns automatically.</para>
    ''' </remarks>
    Public Function GetLookupDictionary(ByVal elementType As Type) As ListDictionary
        For Each es As EntitySet In EntitySets
            If es.ElementType = elementType Then
                Return es.LookupDictionary
            End If
        Next
        Return Nothing
    End Function

    ''' <summary>
    ''' Creates an IBindingList based on a given query.
    ''' </summary>
    ''' <param name="query"><see cref="IEnumerable"/> used as a data source for the list.</param>
    ''' <returns>An <see cref="IBindingList"/> that provides a sortable/filterable view of the data.</returns>
    Public Function CreateView(ByVal query As IEnumerable) As IBindingList

        'Get the query type
        Dim type As Type = GetType(Object)
        For Each obj In query
            type = obj.GetType()
            Exit For
        Next

        'Create the binding list
        Dim listType = GetType(EntityBindingList(Of ))
        listType = listType.MakeGenericType(type)
        Return DirectCast(Activator.CreateInstance(listType, Me, query, type.Name), IEntityBindingList)
    End Function

    ''' <summary>
    ''' Gets a value that determines whether the component is in design mode.
    ''' </summary>
    Protected Friend Shadows ReadOnly Property DesignMode() As Boolean
        Get
            Return MyBase.DesignMode
        End Get
    End Property
#End Region

    '-------------------------------------------------------------------------
#Region "Implementation"

    ''' <summary>
    ''' Populates the EntitySets collection from the current DomainContext.
    ''' </summary>
    Private Sub GenerateEntitySets(ByVal ctxType As Type)
        m_EntitySetCollection.Clear()
        If ctxType IsNot Nothing Then
            Dim ctx = ObjectContext
            For Each pi As PropertyInfo In ctxType.GetProperties()
                Dim type = pi.PropertyType
                If type.IsGenericType AndAlso type.GetGenericTypeDefinition() Is GetType(ObjectSet(Of )) AndAlso type.GetGenericArguments().Length = 1 Then
                    Dim objSet = New EntitySet(Me, pi)
                    m_EntitySetCollection.Add(objSet)
                End If
            Next
        End If
    End Sub

#End Region

#Region "Events"

    ''' <summary>
    ''' Occurs before the value of the <see cref="ObjectContextType"/> property changes.
    ''' </summary>
    Public Event ObjectContextTypeChanging As EventHandler

    ''' <summary>
    ''' Raises the <see cref="ObjectContextTypeChanging"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="EventArgs"/> that contains the event parameters.</param>
    ''' 
    Protected Overridable Sub OnObjectContextTypeChanging(ByVal e As EventArgs)
        RaiseEvent ObjectContextTypeChanging(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs after the value of the <see cref="ObjectContextType"/> property changes.
    ''' </summary>
    Public Event ObjectContextTypeChanged As EventHandler

    ''' <summary>
    ''' Raises the <see cref="ObjectContextTypeChanged"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="EventArgs"/> that contains the event parameters.</param>
    Protected Overridable Sub OnObjectContextTypeChanged(ByVal e As EventArgs)
        RaiseEvent ObjectContextTypeChanged(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs before the value of the <see cref="ObjectContext"/> property changes.
    ''' </summary>
    Public Event ObjectContextChanging As EventHandler
    ''' <summary>
    ''' Raises the <see cref="ObjectContextChanging"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="EventArgs"/> that contains the event parameters.</param>
    ''' 
    Protected Overridable Sub OnObjectContextChanging(ByVal e As EventArgs)
        RaiseEvent ObjectContextChanging(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs after the value of the <see cref="ObjectContext"/> property changes.
    ''' </summary>
    Public Event ObjectContextChanged As EventHandler

    ''' <summary>
    ''' Raises the <see cref="ObjectContextChanged"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="EventArgs"/> that contains the event parameters.</param>
    ''' 
    Protected Overridable Sub OnObjectContextChanged(ByVal e As EventArgs)
        RaiseEvent ObjectContextChanged(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs before changes are saved to the database.
    ''' </summary>
    ''' 
    Public Event SavingChanges As CancelEventHandler
    ''' <summary>
    ''' Raises the <see cref="SavingChanges"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="CancelEventArgs"/> that contains the event parameters.</param>
    ''' 
    Protected Overridable Sub OnSavingChanges(ByVal e As CancelEventArgs)
        RaiseEvent SavingChanges(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs after changes are saved to the database.
    ''' </summary>
    ''' 
    Public Event SavedChanges As EventHandler
    ''' <summary>
    ''' Raises the <see cref="SavedChanges"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="EventArgs"/> that contains the event parameters.</param>
    ''' 
    Protected Overridable Sub OnSavedChanges(ByVal e As EventArgs)
        RaiseEvent SavedChanges(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs before changes are canceled and values are reloaded from the database.
    ''' </summary>
    Public Event CancelingChanges As CancelEventHandler

    ''' <summary>
    ''' Raises the <see cref="CancelingChanges"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="CancelEventArgs"/> that contains the event parameters.</param>
    ''' 
    Protected Overridable Sub OnCancelingChanges(ByVal e As CancelEventArgs)
        RaiseEvent CancelingChanges(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs after changes are canceled and values are reloaded from the database.
    ''' </summary>
    Public Event CanceledChanges As EventHandler

    ''' <summary>
    ''' Raises the <see cref="CanceledChanges"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="EventArgs"/> that contains the event parameters.</param>
    Protected Overridable Sub OnCanceledChanges(ByVal e As EventArgs)
        RaiseEvent CanceledChanges(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs before values are refreshed from the database.
    ''' </summary>
    Public Event Refreshing As CancelEventHandler

    ''' <summary>
    ''' Raises the <see cref="Refreshing"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="CancelEventArgs"/> that contains the event parameters.</param>
    Protected Overridable Sub OnRefreshing(ByVal e As CancelEventArgs)
        RaiseEvent Refreshing(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs after values are refreshed from the database.
    ''' </summary>
    Public Event Refreshed As EventHandler

    ''' <summary>
    ''' Raises the <see cref="Refreshed"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="EventArgs"/> that contains the event parameters.</param>
    Protected Overridable Sub OnRefreshed(ByVal e As EventArgs)
        RaiseEvent Refreshed(Me, e)
    End Sub

    ''' <summary>
    ''' Occurs when an error is detected while loading data from or saving data to the database.
    ''' </summary>
    Public Event DataError As EventHandler(Of DataErrorEventArgs)

    ''' <summary>
    ''' Raises the <see cref="DataError"/> event.
    ''' </summary>
    ''' <param name="e"><see cref="DataErrorEventArgs"/> that contains the event parameters.</param>
    Protected Overridable Sub OnDataError(ByVal e As DataErrorEventArgs)
        RaiseEvent DataError(Me, e)
        If Not e.Handled Then
            Throw e.Exception
        End If
    End Sub

#End Region

#Region "IListSource"

    Private ReadOnly Property ContainsListCollection() As Boolean Implements IListSource.ContainsListCollection
        Get
            Return True
        End Get
    End Property

    Private Function GetList() As IList Implements IListSource.GetList
        ' This method is completely undocumented in MSDN.
        '
        ' Instead of returning a list of the lists available (nice and logical),
        ' you should return a list with ONE object, which exposes the lists available 
        ' as if they were properties of this ONE object. UGH!
        '
        Dim list = New List(Of EntitySetTypeDescriptor)()
        list.Add(New EntitySetTypeDescriptor(Me))
        Return list
    End Function

#End Region

#Region "IExtenderProvider"

    ''' <summary>
    ''' We can extend DataGridView and C1FlexGrid controls.
    ''' </summary>
    ''' <param name="extendee"></param>
    ''' <returns></returns>
    Private Function CanExtend(ByVal extendee As Object) As Boolean Implements IExtenderProvider.CanExtend
        If TypeOf extendee Is Control Then
            Dim type = extendee.GetType()
            While type IsNot Nothing
                'DataGridView
                If type Is GetType(DataGridView) Then
                    Return True
                End If

                'C1FlexGrid
                Dim typeName = type.ToString()
                If typeName.IndexOf("C1FlexGrid", StringComparison.Ordinal) > -1 Then
                    Return True
                End If
                type = type.BaseType
            End While
        End If
        Return False
    End Function

    ''' <summary>
    ''' Add or remove automatic data maps for the columns on a C1FlexGrid control.
    ''' </summary>
    ''' <param name="grid">DataGridView or C1FlexGrid control.</param>
    ''' <param name="autoLookups">Whether to enable or disable automatic data maps for the columns on the <paramref name="grid"/> control.</param>
    <EditorBrowsable(EditorBrowsableState.Never)>
    Public Sub SetAutoLookup(ByVal grid As Control, ByVal autoLookups As Boolean)
        Dim oldAutoLookups = m_AutoLookup.ContainsKey(grid)
        If oldAutoLookups <> autoLookups Then
            If autoLookups Then
                m_AutoLookup.Add(grid, True)
            Else
                m_AutoLookup.Remove(grid)
            End If
            If Not DesignMode Then
                EnableAutoLookup(grid, autoLookups)
            End If
        End If
    End Sub

    ''' <summary>
    '''Gets a value that determines whether automatic data maps are enabled for a given C1FlexGrid control.
    ''' </summary>
    ''' <param name="grid">DataGridView or C1FlexGrid control.</param>
    ''' <returns>Whether automatic data maps are is enabled for the columns on the <paramref name="grid"/> control.</returns>
    <EditorBrowsable(EditorBrowsableState.Never), DefaultValue(False)>
    Public Function GetAutoLookup(ByVal grid As Control) As Boolean
        Return m_AutoLookup.ContainsKey(grid)
    End Function

    'Enable/disable automatic data maps for a control
    Private m_AutoLookup As New Dictionary(Of Control, Boolean)()

    Private Shared Sub EnableAutoLookup(ByVal control As Control, map As Boolean)
        'Get event handlers
        Dim ctlType = control.GetType()
        Dim dsChanged = ctlType.GetEvent("DataSourceChanged")
        Dim dmChanged = ctlType.GetEvent("DataMemberChanged")
        Dim bcChanged = ctlType.GetEvent("BindingContextChanged")

        'Sanity
        If dsChanged Is Nothing OrElse dmChanged Is Nothing OrElse bcChanged Is Nothing Then
            Throw New Exception("Cannot connect event handlers for this control.")
        End If

        'Connect/disconnect event handlers
        Dim handler = New EventHandler(AddressOf ControlDataSourceChanged)
        If map Then
            dsChanged.AddEventHandler(control, handler)
            dmChanged.AddEventHandler(control, handler)
            bcChanged.AddEventHandler(control, handler)
        Else
            dsChanged.RemoveEventHandler(control, handler)
            dmChanged.RemoveEventHandler(control, handler)
            bcChanged.RemoveEventHandler(control, handler)
        End If
    End Sub

    Private Shared Sub ControlDataSourceChanged(ByVal sender As Object, ByVal e As EventArgs)
        CustomizeGrid(CType(TryCast(sender, Control), DataGridView))
    End Sub

    'Customize the columns of a DataGridView or C1FlexGrid control 
    'Use dynamic type for C1FlexGrid in order to avoid dependencies.
    Private Shared Sub CustomizeGrid(ByVal dgv As DataGridView)
        Dim grid = DirectCast(dgv, DataGridView)

        If grid.DataSource IsNot Nothing AndAlso grid.BindingContext IsNot Nothing Then
            'Get currency manager
            Dim cm As CurrencyManager = Nothing
            Try
                cm = TryCast(grid.BindingContext(grid.DataSource, grid.DataMember), CurrencyManager)
            Catch
            End Try

            'Get source list from currency manager bound directly to EntityDataSource
            Dim list = If(cm IsNot Nothing, TryCast(cm.List, IEntityBindingList), Nothing)

            ' if failed, try binding via BindingSource component
            If list Is Nothing AndAlso TypeOf cm.List Is BindingSource Then
                list = TryCast(DirectCast(cm.List, BindingSource).List, IEntityBindingList)
            End If

            ' customize the columns
            If list IsNot Nothing AndAlso list.DataSource IsNot Nothing Then
                Dim entType = list.ElementType
                Dim entDataSource = list.DataSource

                'Customize grid columns
                ConfigureGridColumns(grid, entType, entDataSource)
            End If
        End If
    End Sub

    'Customize the columns of the DataGridView
    Private Shared Sub ConfigureGridColumns(ByVal dgv As DataGridView, ByVal entType As Type, ByVal entDataSource As EntityDataSource)
        For colIndex As Integer = 0 To dgv.Columns.Count - 1
            'Get column
            Dim c = dgv.Columns(colIndex)

            'If the column is a key, make it read-only
            Dim pi = entType.GetProperty(c.DataPropertyName)
            If pi IsNot Nothing Then
                Dim atts = pi.GetCustomAttributes(GetType(EdmScalarPropertyAttribute), False)
                If atts.Length > 0 Then
                    Dim att = TryCast(atts(0), EdmScalarPropertyAttribute)
                    If att.EntityKeyProperty Then
                        c.ReadOnly = True
                    End If
                End If

                'If the column holds entities, give it a data map
                Dim type = pi.PropertyType
                If GetType(EntityObject).IsAssignableFrom(type) Then
                    Dim map = entDataSource.GetLookupDictionary(type)
                    If map IsNot Nothing Then
                        Dim col = New DataGridViewComboBoxColumn()
                        col.HeaderText = c.HeaderText
                        col.DataPropertyName = c.DataPropertyName
                        col.Width = c.Width
                        col.DefaultCellStyle = c.DefaultCellStyle
                        col.DataSource = map
                        col.ValueMember = "Key"
                        col.DisplayMember = "Value"
                        dgv.Columns.RemoveAt(colIndex)
                        dgv.Columns.Insert(colIndex, col)
                    End If
                End If
            End If
        Next
    End Sub

#End Region

End Class

''' <summary>
''' Provides data for the <see /> event.
''' </summary>
Public Class DataErrorEventArgs
    Inherits EventArgs

    Private m_Exception As Exception
    Private m_Handled As Boolean

    ''' <summary>
    '''Gets or sets the <see cref="Exception"/> that triggered the event.
    ''' </summary>
    Public Property Exception() As Exception
        Get
            Return m_Exception
        End Get
        Set(ByVal value As Exception)
            m_Exception = value
        End Set
    End Property

    ''' <summary>
    ''' Whether the error was handled and the source exception should be ignored.
    ''' </summary>
    Public Property Handled() As Boolean
        Get
            Return m_Handled
        End Get
        Set(ByVal value As Boolean)
            m_Handled = value
        End Set
    End Property

    ''' <summary>
    ''' Initializes a new instance of a <see cref="DataErrorEventArgs"/>.
    ''' </summary>
    ''' <param name="x"><see cref="Exception"/> that triggered the event.</param>
    Public Sub New(ByVal x As Exception)
        Exception = x
    End Sub

End Class
